#include <fitz-internal.h>
#include <mucbz.h>
#include <mupdf.h>
#include <muxps.h>

#include <pthread.h>

#include <jni.h>

#define OP_DOC_TYPE_CBZ 1
#define OP_DOC_TYPE_PDF 2
#define OP_DOC_TYPE_XPS 3

#define OP_MAX_PAGES 2

#define OP_FZ_STORE_MAX 67108864

#define OP_LOG_I(...) __android_log_print(ANDROID_LOG_INFO, __FILE__, __VA_ARGS__)
#define OP_LOG_E(...) __android_log_print(ANDROID_LOG_ERROR, __FILE__, __VA_ARGS__)
#define OP_FAIL(...) do { OP_LOG_E(__VA_ARGS__); abort(); }  while (0)

#define OP_MALLOC_STRUCT(s) op_calloc(1, sizeof(s))

typedef struct op_mupdf_ctx_s op_mupdf_ctx;
struct op_mupdf_ctx_s {
    jobject doc_source;
    pthread_mutex_t mutexes[FZ_LOCK_MAX];
    fz_context *fz_ctx;
    fz_document *doc;

    fz_display_list *display_lists[OP_MAX_PAGES];
    fz_page *pages[OP_MAX_PAGES];
    unsigned char *pixels[OP_MAX_PAGES];
    fz_pixmap *pixmaps[OP_MAX_PAGES];

    fz_cookie cookies[OP_MAX_PAGES];
    fz_rect page_bounds[OP_MAX_PAGES];
    fz_locks_context fz_locks;
    int render_page_count;
    int render_nr;
    int render_indexes[OP_MAX_PAGES];
    fz_bbox bboxes[OP_MAX_PAGES];
    fz_matrix transforms[OP_MAX_PAGES];
};

typedef struct op_render_task_s op_render_task;
struct op_render_task_s {
    op_mupdf_ctx *ctx;
    int n;
};

static int op_call_int_method(JNIEnv *env, jobject obj, jmethodID method_id, ...);
static jobject op_call_object_method(JNIEnv *env, jobject obj, jmethodID method_id, ...);
static void op_call_void_method(JNIEnv *env, jobject obj, jmethodID method_id, ...);
static void *op_calloc(size_t num, size_t size);
static void op_delete_global_ref(JNIEnv *env, jobject global_ref);
static void op_destroy_fz_locks(op_mupdf_ctx *ctx);
static void op_fail_on_exception(JNIEnv *env);
static jclass op_find_class(JNIEnv *env, char const *class_name);
static void op_free_display_lists(op_mupdf_ctx *ctx);
static void op_free_pages(op_mupdf_ctx *ctx);
static void op_free_pixels(op_mupdf_ctx *ctx);
static void op_free_pixmaps(op_mupdf_ctx *ctx);
static void op_fz_lock(void *user, int lock);
static void op_fz_unlock(void *user, int lock);
static JNIEnv *op_get_env(void);
static jmethodID op_get_method_id(JNIEnv *env, jclass clazz, char const *name, char const *sig);
static void op_init_fz_locks(op_mupdf_ctx *ctx);
static void *op_malloc(size_t size);
static void op_mupdf_clear(op_mupdf_ctx *ctx);
static jboolean op_native_authenticate_password(JNIEnv *env, jobject self, jlong mupdf_ctx, jstring password);
static void op_native_blit_to_texture(JNIEnv *env, jobject self, jlong pixmap, jint width, jint left, jint top,
		jint right, jint bottom, jobject texture);
static void op_native_cancel_render(JNIEnv *env, jobject self, jlong mupdf_ctx);
static void op_native_clear_mupdf(JNIEnv *env, jobject self, jlong mupdf_ctx);
static void op_native_delete_mupdf_ctx(JNIEnv *env, jobject self, jlong mupdf_ctx);
static jlong op_native_fetch_pixmap(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index);
static void op_native_free_pixmap(JNIEnv *env, jobject self, jlong pixels);
static jint op_native_get_page_count(JNIEnv *env, jobject self, jlong mupdf_ctx);
static jfloat op_native_get_page_width(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index);
static jfloat op_native_get_page_height(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index);
static void op_native_load_page(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index, jint page_nr);
static jboolean op_native_needs_password(JNIEnv *env, jobject self, jlong mupdf_ctx);
static jlong op_native_new_mupdf_ctx(JNIEnv *env, jobject self, jobject doc_source);
static void op_native_prepare_render(JNIEnv *env, jobject self, jlong mupdf_ctx, jint count);
static void op_native_render_page(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index, jint width, jint height,
		jint left, jint top, jint right, jint bottom);
static void op_new_fz_context(op_mupdf_ctx *ctx);
static fz_stream *op_new_fz_stream(JNIEnv *env, op_mupdf_ctx *ctx);
static jobject op_new_global_ref(JNIEnv *env, jobject obj);
static fz_document *op_open_cbz(fz_stream *stream);
static void op_open_doc(JNIEnv *env, op_mupdf_ctx *ctx);
static void op_open_doc_source(JNIEnv *env, jobject doc_source);
static fz_document *op_open_pdf(fz_stream *stream);
static fz_document *op_open_xps(fz_stream *stream);
static void *op_render_page(void *t);
static void op_render_pages(op_mupdf_ctx *ctx);
static void op_stream_close(fz_context *ctx, void *state);
static int op_stream_read(fz_stream *stm, unsigned char *buf, int len);
static void op_stream_seek(fz_stream *stm, int offset, int whence);

static jmethodID op_close_method_id;
static jmethodID op_get_buffer_method_id;
static jmethodID op_get_doc_type_method_id;
static JNINativeMethod const op_native_methods[] = {
        {"authenticatePassword", "(JLjava/lang/String;)Z", op_native_authenticate_password},
        {"blitToTexture", "(JIIIIILjava/nio/ByteBuffer;)V", op_native_blit_to_texture},
        {"cancelRender", "(J)V", op_native_cancel_render},
        {"clearMupdf", "(J)V", op_native_clear_mupdf},
        {"deleteMupdfCtx", "(J)V", op_native_delete_mupdf_ctx},
        {"fetchPixmap", "(JI)J", op_native_fetch_pixmap},
        {"freePixmap", "(J)V", op_native_free_pixmap},
        {"getPageCount", "(J)I", op_native_get_page_count},
        {"getPageWidth", "(JI)F", op_native_get_page_width},
        {"getPageHeight", "(JI)F", op_native_get_page_height},
        {"loadPage", "(JII)V", op_native_load_page},
        {"needsPassword", "(J)Z", op_native_needs_password},
        {"newMupdfCtx", "(Lde/geraldthaler/octopuspdf/docrender/DocSource;)J", op_native_new_mupdf_ctx},
        {"prepareRender", "(JI)V", op_native_prepare_render},
        {"renderPage", "(JIIIIIII)V", op_native_render_page}
};
static jmethodID op_open_method_id;
static jmethodID op_print_stack_trace_method_id;
static jmethodID op_read_method_id;
static jmethodID op_seek_method_id;
static JavaVM *op_vm;

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    jclass classDocSource, classNative, classThrowable;
    JNIEnv *env;

    op_vm = vm;
    env = op_get_env();
    classNative = op_find_class(env, "de/geraldthaler/octopuspdf/Native");
    if ((*env)->RegisterNatives(env, classNative, op_native_methods, nelem(op_native_methods)) != JNI_OK)
        OP_FAIL("RegisterNatives() failed");
    classDocSource = op_find_class(env, "de/geraldthaler/octopuspdf/docrender/DocSource");
    op_close_method_id = op_get_method_id(env, classDocSource, "close", "()V");
    op_get_buffer_method_id = op_get_method_id(env, classDocSource, "getBuffer", "()[B");
    op_get_doc_type_method_id = op_get_method_id(env, classDocSource, "getDocType", "()I");
    op_open_method_id = op_get_method_id(env, classDocSource, "open", "()V");
    op_read_method_id = op_get_method_id(env, classDocSource, "read", "(I)I");
    op_seek_method_id = op_get_method_id(env, classDocSource, "seek", "(II)I");
    classThrowable = op_find_class(env, "java/lang/Throwable");
    op_print_stack_trace_method_id = op_get_method_id(env, classThrowable, "printStackTrace", "()V");
    return JNI_VERSION_1_6;
}

static int op_call_int_method(JNIEnv *env, jobject obj, jmethodID method_id, ...) {
    int r;
    va_list args;
    va_start(args, method_id);
    r = (*env)->CallIntMethodV(env, obj, method_id, args);
    va_end(args);
    op_fail_on_exception(env);
    return r;
}

static jobject op_call_object_method(JNIEnv *env, jobject obj, jmethodID method_id, ...) {
    jobject r;
    va_list args;
    va_start(args, method_id);
    r = (*env)->CallObjectMethodV(env, obj, method_id, args);
    va_end(args);
    op_fail_on_exception(env);
    return r;
}

static void op_call_void_method(JNIEnv *env, jobject obj, jmethodID method_id, ...) {
    va_list args;
    va_start(args, method_id);
    (*env)->CallVoidMethodV(env, obj, method_id, args);
    va_end(args);
    op_fail_on_exception(env);
}

static void *op_calloc(size_t num, size_t size) {
    void *p = calloc(num, size);

    if (!p)
        OP_FAIL("calloc() failed");
    return p;
}

static void op_delete_global_ref(JNIEnv *env, jobject global_ref) {
    if (global_ref != NULL)
        (*env)->DeleteGlobalRef(env, global_ref);
}

static void op_destroy_fz_locks(op_mupdf_ctx *ctx) {
    int n;

    for (n = 0; n < FZ_LOCK_MAX; ++n) {
        if (pthread_mutex_destroy(&ctx->mutexes[n]))
            OP_FAIL("pthread_mutex_destroy() failed");
    }
}

static void op_fail_on_exception(JNIEnv *env) {
    jthrowable ex = (*env)->ExceptionOccurred(env);

    if (ex) {
        (*env)->ExceptionClear(env);
        (*env)->CallVoidMethod(env, ex, op_print_stack_trace_method_id);
        OP_FAIL("java exception");
    }
}

static void op_free_display_lists(op_mupdf_ctx *ctx) {
	int n;

    for (n = 0; n < OP_MAX_PAGES; ++n) {
    	fz_free_display_list(ctx->fz_ctx, ctx->display_lists[n]);
    	ctx->display_lists[n] = NULL;
    }
}

static void op_free_pages(op_mupdf_ctx *ctx) {
	int n;

    for (n = 0; n < OP_MAX_PAGES; ++n) {
    	fz_free_page(ctx->doc, ctx->pages[n]);
    	ctx->pages[n] = NULL;
    }
}

static void op_free_pixels(op_mupdf_ctx *ctx) {
	int n;

    for (n = 0; n < OP_MAX_PAGES; ++n) {
    	free(ctx->pixels[n]);
    	ctx->pixels[n] = NULL;
    }
}

static void op_free_pixmaps(op_mupdf_ctx *ctx) {
	int n;

    for (n = 0; n < OP_MAX_PAGES; ++n) {
    	fz_drop_pixmap(ctx->fz_ctx, ctx->pixmaps[n]);
    	ctx->pixmaps[n] = NULL;
    }
}

static jclass op_find_class(JNIEnv *env, char const *class_name) {
    jclass clazz = (*env)->FindClass(env, class_name);

    if (!clazz)
        OP_FAIL("FindClass() failed");
    return clazz;
}

static void op_fz_lock(void *user, int lock) {
    pthread_mutex_t *mutexes = (pthread_mutex_t *) user;

    if (pthread_mutex_lock(&mutexes[lock]))
        OP_FAIL("pthread_mutex_lock() failed");
}

static void op_fz_unlock(void *user, int lock) {
    pthread_mutex_t *mutexes = (pthread_mutex_t *) user;

    if (pthread_mutex_unlock(&mutexes[lock]))
        OP_FAIL("pthread_mutex_unlock() failed");
}

static JNIEnv *op_get_env(void) {
    JNIEnv *env;
    if ((*op_vm)->GetEnv(op_vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK)
        OP_FAIL("GetEnv() failed");
    return env;
}

static jmethodID op_get_method_id(JNIEnv *env, jclass clazz, char const *name, char const *sig) {
    jmethodID method_id = (*env)->GetMethodID(env, clazz, name, sig);

    if (!method_id)
        OP_FAIL("GetMethodID() failed, name = %s, sig = %s", name, sig);
    return method_id;
}

static void op_init_fz_locks(op_mupdf_ctx *ctx) {
    int n;

    for (n = 0; n < FZ_LOCK_MAX; ++n) {
        if (pthread_mutex_init(&ctx->mutexes[n], NULL))
            OP_FAIL("pthread_mutex_init() failed");
    }

    ctx->fz_locks.user = ctx->mutexes;
    ctx->fz_locks.lock = op_fz_lock;
    ctx->fz_locks.unlock = op_fz_unlock;
}

static void *op_malloc(size_t size) {
    void *p = malloc(size);

    if (!p)
        OP_FAIL("malloc() failed");
    return p;
}

static void op_mupdf_clear(op_mupdf_ctx *ctx) {
    op_free_pixmaps(ctx);
    op_free_pixels(ctx);
    op_free_pages(ctx);
    op_free_display_lists(ctx);
}

static jboolean op_native_authenticate_password(JNIEnv *env, jobject self, jlong mupdf_ctx, jstring password) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;
    char const *pw;
	int r;
	pw = (*env)->GetStringUTFChars(env, password, NULL);
	if (pw == NULL)
		return JNI_FALSE;
	r = fz_authenticate_password(ctx->doc, (char *) pw);
	(*env)->ReleaseStringUTFChars(env, password, pw);
	return r;
}

static void op_native_blit_to_texture(JNIEnv *env, jobject self, jlong pixmap, jint width, jint left, jint top,
		jint right, jint bottom, jobject texture) {
	unsigned char *src = (unsigned char *) (intptr_t) pixmap;
	unsigned char *dest = (*env)->GetDirectBufferAddress(env, texture);
	int y, w, s;

	src += 4 * (width * top + left);
	w = 4 * (right - left);
	s = 4 * width;
	for (y = top; y < bottom; ++y) {
		memcpy(dest, src, w);
		src += s;
		dest += w;
	}
}

static void op_native_cancel_render(JNIEnv *env, jobject self, jlong mupdf_ctx) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;
    int n;

    for (n = 0; n < OP_MAX_PAGES; ++n)
    	ctx->cookies[n].abort = 1;
}


static void op_native_clear_mupdf(JNIEnv *env, jobject self, jlong mupdf_ctx) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;

    op_mupdf_clear(ctx);
}

static void op_native_delete_mupdf_ctx(JNIEnv *env, jobject self, jlong mupdf_ctx) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;

    if (!ctx)
        return;
    op_mupdf_clear(ctx);

    fz_close_document(ctx->doc);
    fz_free_context(ctx->fz_ctx);
    op_destroy_fz_locks(ctx);
    op_delete_global_ref(env, ctx->doc_source);
    free(ctx);
}

static jlong op_native_fetch_pixmap(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;
    jlong r;

    r = (intptr_t) ctx->pixels[index];
    ctx->pixels[index] = NULL;
    return r;
}

static void op_native_free_pixmap(JNIEnv *env, jobject self, jlong pixels) {
	free((void *) (intptr_t) pixels);
}

static jint op_native_get_page_count(JNIEnv *env, jobject self, jlong mupdf_ctx) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;
    jint r;

    fz_try (ctx->fz_ctx) {
        r = fz_count_pages(ctx->doc);
    } fz_catch (ctx->fz_ctx) {
        OP_FAIL("fz_count_pages() failed");
    }
    return r;
}

static jfloat op_native_get_page_width(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;

    return ctx->page_bounds[index].x1;
}

static jfloat op_native_get_page_height(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;

    return ctx->page_bounds[index].y1;
}

static void op_native_load_page(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index, jint page_nr) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;

    fz_free_page(ctx->doc, ctx->pages[index]);

    fz_try (ctx->fz_ctx) {
    	ctx->pages[index] = fz_load_page(ctx->doc, page_nr);
    } fz_catch (ctx->fz_ctx) {
        OP_FAIL("fz_load_page() failed");
    }
    ctx->page_bounds[index] = fz_bound_page(ctx->doc, ctx->pages[index]);
    if (ctx->page_bounds[index].x0 != 0.0f || ctx->page_bounds[index].y0 != 0.0f)
        OP_FAIL("fz_bound_page(): (x0, y0) != (0, 0)");
}

static jboolean op_native_needs_password(JNIEnv *env, jobject self, jlong mupdf_ctx) {
    jboolean r;
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;

    fz_try (ctx->fz_ctx) {
        r = fz_needs_password(ctx->doc) ? JNI_TRUE : JNI_FALSE;
    } fz_catch (ctx->fz_ctx) {
        OP_FAIL("fz_needs_password() failed");
    } 
    return r;
}

static void op_native_prepare_render(JNIEnv *env, jobject self, jlong mupdf_ctx, jint count) {
    op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;
    int n;

    ctx->render_nr = 0;
	ctx->render_page_count = count;
	for (n = 0; n < OP_MAX_PAGES; ++n)
		ctx->cookies[n].abort = 0;
}

static jlong op_native_new_mupdf_ctx(JNIEnv *env, jobject self, jobject doc_source) {
    op_mupdf_ctx *ctx = OP_MALLOC_STRUCT(op_mupdf_ctx);

    ctx->doc_source = op_new_global_ref(env, doc_source);
    op_init_fz_locks(ctx);
    op_new_fz_context(ctx);
    op_open_doc(env, ctx);
    return (intptr_t) ctx;
}

static void op_native_render_page(JNIEnv *env, jobject self, jlong mupdf_ctx, jint index, jint width, jint height,
		jint left, jint top, jint right, jint bottom) {
	op_mupdf_ctx *ctx = (op_mupdf_ctx *) (intptr_t) mupdf_ctx;
	fz_device *list_dev;

	fz_try(ctx->fz_ctx) {
		ctx->display_lists[index] = fz_new_display_list(ctx->fz_ctx);
	} fz_catch(ctx->fz_ctx) {
		OP_FAIL("fz_new_display_list() failed");
	}
	fz_try(ctx->fz_ctx) {
		list_dev = fz_new_list_device(ctx->fz_ctx, ctx->display_lists[index]);
	} fz_catch(ctx->fz_ctx) {
		OP_FAIL("fz_new_list_device() failed");
	}
	fz_try(ctx->fz_ctx) {
		fz_run_page(ctx->doc, ctx->pages[index], list_dev, fz_identity, &ctx->cookies[index]);
	} fz_catch(ctx->fz_ctx) {
		OP_FAIL("fz_run_page() failed");
	}
	fz_free_device(list_dev);
	fz_free_page(ctx->doc, ctx->pages[index]);
	ctx->pages[index] = NULL;
	if (ctx->cookies[index].abort)
		return;
	ctx->bboxes[index].x0 = left;
	ctx->bboxes[index].y0 = top;
	ctx->bboxes[index].x1 = right;
	ctx->bboxes[index].y1 = bottom;
	ctx->pixels[index] = op_malloc(4 * (right - left) * (bottom - top));
	fz_try(ctx->fz_ctx) {
		ctx->pixmaps[index] = fz_new_pixmap_with_bbox_and_data(ctx->fz_ctx, fz_device_rgb, ctx->bboxes[index],
				ctx->pixels[index]);
	} fz_catch(ctx->fz_ctx) {
		OP_FAIL("fz_new_pixmap_with_bbox_and_data() failed");
	}
	fz_clear_pixmap_with_value(ctx->fz_ctx, ctx->pixmaps[index], 0xff);
	ctx->transforms[index] = fz_scale(width / ctx->page_bounds[index].x1, height / ctx->page_bounds[index].y1);
	ctx->render_indexes[ctx->render_nr++] = index;
	if (ctx->render_nr == ctx->render_page_count)
		op_render_pages(ctx);
}

static void op_new_fz_context(op_mupdf_ctx *ctx) {
    ctx->fz_ctx = fz_new_context(NULL, &ctx->fz_locks, OP_FZ_STORE_MAX);

    if (!ctx->fz_ctx)
        OP_FAIL("fz_new_context() failed");
    fz_set_aa_level(ctx->fz_ctx, 8);
}

static fz_stream *op_new_fz_stream(JNIEnv *env, op_mupdf_ctx *ctx) {
    fz_stream *stream;

    op_open_doc_source(env, ctx->doc_source);
    fz_try (ctx->fz_ctx) {
        stream = fz_new_stream(ctx->fz_ctx, ctx->doc_source, op_stream_read, op_stream_close);
    } fz_catch(ctx->fz_ctx) {
        OP_FAIL("fz_new_stream() failed");
    }
    stream->seek = op_stream_seek;
    return stream;
}

static jobject op_new_global_ref(JNIEnv *env, jobject obj) {
    jobject global = (*env)->NewGlobalRef(env, obj);

    if (!global)
        OP_FAIL("NewGlobalRef() failed");
    return global;
}

static fz_document *op_open_cbz(fz_stream *stream) {
    fz_context *ctx = stream->ctx;
    fz_document *doc;

    fz_try (ctx) {
        doc = (fz_document *) cbz_open_document_with_stream(stream);
    } fz_catch (ctx) {
        OP_FAIL("cbz_open_document_with_stream() failed");
    }
    return doc;
}

static void op_open_doc(JNIEnv *env, op_mupdf_ctx *ctx) {
    fz_stream *stream = op_new_fz_stream(env, ctx);

    switch (op_call_int_method(env, ctx->doc_source, op_get_doc_type_method_id)) {
    case OP_DOC_TYPE_CBZ:
        ctx->doc = op_open_cbz(stream);
        break;
    case OP_DOC_TYPE_XPS:
        ctx->doc = op_open_xps(stream);
        break;
    default:
        ctx->doc = op_open_pdf(stream);
        break;
    }
    fz_close(stream);
}

static void op_open_doc_source(JNIEnv *env, jobject doc_source) {
    op_call_void_method(env, doc_source, op_open_method_id);
}

static fz_document *op_open_pdf(fz_stream *stream) {
    fz_context *ctx = stream->ctx;
    fz_document *doc;

    fz_try (ctx) {
        doc = (fz_document *) pdf_open_document_with_stream(stream);
    } fz_catch (ctx) {
        OP_FAIL("pdf_open_document_with_stream() failed");
    }
    return doc;
}

static fz_document *op_open_xps(fz_stream *stream) {
    fz_context *ctx = stream->ctx;
    fz_document *doc;

    fz_try (ctx) {
        doc = (fz_document *) xps_open_document_with_stream(stream);
    } fz_catch (ctx) {
        OP_FAIL("xps_open_document_with_stream() failed");
    }
    return doc;
}

static void *op_render_page(void *t) {
	op_render_task *task = (op_render_task *) t;
	op_mupdf_ctx *ctx = task->ctx;
	fz_context *fz_ctx = ctx->fz_ctx;
	fz_device *dev;
	int index = ctx->render_indexes[task->n];

	fz_ctx = fz_clone_context(fz_ctx);
	if (!fz_ctx)
		OP_FAIL("fz_clone_context() failed");
	fz_try (fz_ctx) {
		dev = fz_new_draw_device(fz_ctx, ctx->pixmaps[index]);
	} fz_catch(fz_ctx) {
		OP_FAIL("fz_new_draw_device() failed");
	}
	fz_try (fz_ctx) {
		fz_run_display_list(ctx->display_lists[index], dev, ctx->transforms[index], ctx->bboxes[index],
				&ctx->cookies[index]);
	} fz_catch(fz_ctx) {
		OP_FAIL("fz_run_display_list() failed");
	}
	fz_free_device(dev);
	fz_free_context(fz_ctx);

	return NULL;
}

static void op_render_pages(op_mupdf_ctx *ctx) {
	int n;
	op_render_task render_tasks[OP_MAX_PAGES];
	pthread_t threads[OP_MAX_PAGES - 1];

	for (n = 0; n < ctx->render_page_count - 1; ++n) {
		render_tasks[n].ctx = ctx;
		render_tasks[n].n = n;
		if (pthread_create(&threads[n], NULL, op_render_page, &render_tasks[n]))
			OP_FAIL("pthread_create() failed");
	}
	render_tasks[n].ctx = ctx;
	render_tasks[n].n = n;
	op_render_page(&render_tasks[n]);
	for (n = 0; n < ctx->render_page_count - 1; ++n) {
		void *unused;
		if (pthread_join(threads[n], &unused))
			OP_FAIL("pthread_join() failed");
	}
    op_free_pixmaps(ctx);
    op_free_pages(ctx);
    op_free_display_lists(ctx);
}

static void op_stream_close(fz_context *ctx, void *state) {
    JNIEnv *env = op_get_env();
    jobject doc_source = (jobject) state;

    op_call_void_method(env, doc_source, op_close_method_id);
}

static int op_stream_read(fz_stream *stm, unsigned char *buf, int len) {
    JNIEnv *env = op_get_env();
    jobject doc_source = (jobject) stm->state;
    jbyteArray buffer = (jbyteArray) op_call_object_method(env, doc_source, op_get_buffer_method_id);
    int r, size;

    size = 0;
    while (len > 0) {
        r = op_call_int_method(env, doc_source, op_read_method_id, len);
        if (r < 0)
            break;
        if (r == 0)
            continue;
        (*env)->GetByteArrayRegion(env, buffer, 0, r, (jbyte *) buf);
        len -= r;
        buf += r;
        size += r;
    }
    (*env)->DeleteLocalRef(env, buffer);
    return size;
}

static void op_stream_seek(fz_stream *stm, int offset, int whence) {
    JNIEnv *env = op_get_env();
    jobject doc_source = (jobject) stm->state;

    stm->pos = op_call_int_method(env, doc_source, op_seek_method_id, offset, whence);
    stm->rp = stm->bp;
    stm->wp = stm->bp;
}
